/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2010-2015 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* http://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.glassfish.jersey.wadl.doclet;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.namespace.QName;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.AnnotationDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.ClassDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.MethodDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.NamedValueType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.ParamDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.RepresentationDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.RequestDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.ResourceDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.ResponseDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.WadlParamType;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.AnnotationDesc.ElementValuePair;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.MemberDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.ParamTag;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.SeeTag;
import com.sun.javadoc.Tag;
/**
* Creates a resourcedoc XML file.
* <p/>
* <p>
* The ResourceDoc file contains the javadoc documentation
* of resource classes, so that this can be used for extending generated wadl with useful
* documentation.
* </p>
*
* @author <a href="mailto:martin.grotzke@freiheit.com">Martin Grotzke</a>
*/
public class ResourceDoclet {
private static final Pattern PATTERN_RESPONSE_REPRESENTATION = Pattern.compile("@response\\.representation\\.([\\d]+)\\..*");
private static final String OPTION_OUTPUT = "-output";
private static final String OPTION_CLASSPATH = "-classpath";
private static final String OPTION_DOC_PROCESSORS = "-processors";
private static final Logger LOG = Logger.getLogger(ResourceDoclet.class.getName());
/**
* Start the doclet.
*
* @param root the root JavaDoc document.
* @return true if no exception is thrown.
*/
public static boolean start(final RootDoc root) {
final String output = getOptionArg(root.options(), OPTION_OUTPUT);
final String classpath = getOptionArg(root.options(), OPTION_CLASSPATH);
// LOG.info( "Have classpath: " + classpath );
final String[] classpathElements = classpath.split(File.pathSeparator);
final ClassLoader cl = Thread.currentThread().getContextClassLoader();
final ClassLoader ncl = new Loader(classpathElements,
ResourceDoclet.class.getClassLoader());
Thread.currentThread().setContextClassLoader(ncl);
final String docProcessorOption = getOptionArg(root.options(), OPTION_DOC_PROCESSORS);
final String[] docProcessors = docProcessorOption != null ? docProcessorOption.split(":") : null;
final DocProcessorWrapper docProcessor = new DocProcessorWrapper();
try {
if (docProcessors != null && docProcessors.length > 0) {
final Class<?> clazz = Class.forName(docProcessors[0], true, Thread.currentThread().getContextClassLoader());
final Class<? extends DocProcessor> dpClazz = clazz.asSubclass(DocProcessor.class);
docProcessor.add(dpClazz.newInstance());
}
} catch (final Exception e) {
LOG.log(Level.SEVERE, "Could not load docProcessors " + docProcessorOption, e);
}
try {
final ResourceDocType result = new ResourceDocType();
final ClassDoc[] classes = root.classes();
for (final ClassDoc classDoc : classes) {
LOG.fine("Writing class " + classDoc.qualifiedTypeName());
final ClassDocType classDocType = new ClassDocType();
classDocType.setClassName(classDoc.qualifiedTypeName());
classDocType.setCommentText(classDoc.commentText());
docProcessor.processClassDoc(classDoc, classDocType);
for (final MethodDoc methodDoc : classDoc.methods()) {
final MethodDocType methodDocType = new MethodDocType();
methodDocType.setMethodName(methodDoc.name());
methodDocType.setMethodSignature(methodDoc.signature());
methodDocType.setCommentText(methodDoc.commentText());
docProcessor.processMethodDoc(methodDoc, methodDocType);
addParamDocs(methodDoc, methodDocType, docProcessor);
addRequestRepresentationDoc(methodDoc, methodDocType);
addResponseDoc(methodDoc, methodDocType);
classDocType.getMethodDocs().add(methodDocType);
}
result.getDocs().add(classDocType);
}
try {
final Class<?>[] clazzes = getJAXBContextClasses(result, docProcessor);
final JAXBContext c = JAXBContext.newInstance(clazzes);
final Marshaller m = c.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
final OutputStream out = new BufferedOutputStream(new FileOutputStream(output));
final String[] cdataElements = getCDataElements(docProcessor);
final XMLSerializer serializer = getXMLSerializer(out, cdataElements);
m.marshal(result, serializer);
out.close();
LOG.info("Wrote " + output);
} catch (final Exception e) {
LOG.log(Level.SEVERE, "Could not serialize ResourceDoc.", e);
return false;
}
} finally {
Thread.currentThread().setContextClassLoader(cl);
}
return true;
}
private static String[] getCDataElements(final DocProcessor docProcessor) {
final String[] original = new String[] {"ns1^commentText", "ns2^commentText", "^commentText"};
if (docProcessor == null) {
return original;
} else {
final String[] cdataElements = docProcessor.getCDataElements();
if (cdataElements == null || cdataElements.length == 0) {
return original;
} else {
final String[] result = copyOf(original, original.length + cdataElements.length);
for (int i = 0; i < cdataElements.length; i++) {
result[original.length + i] = cdataElements[i];
}
return result;
}
}
}
@SuppressWarnings("unchecked")
private static <T, U> T[] copyOf(final U[] original, final int newLength) {
final T[] copy = (original.getClass() == Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(original.getClass().getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
private static Class<?>[] getJAXBContextClasses(
final ResourceDocType result, final DocProcessor docProcessor) {
final Class<?>[] clazzes;
if (docProcessor == null) {
clazzes = new Class<?>[1];
} else {
final Class<?>[] requiredJaxbContextClasses = docProcessor.getRequiredJaxbContextClasses();
if (requiredJaxbContextClasses != null) {
clazzes = new Class<?>[1 + requiredJaxbContextClasses.length];
for (int i = 0; i < requiredJaxbContextClasses.length; i++) {
clazzes[i + 1] = requiredJaxbContextClasses[i];
}
} else {
clazzes = new Class<?>[1];
}
}
clazzes[0] = result.getClass();
return clazzes;
}
private static XMLSerializer getXMLSerializer(final OutputStream os, final String[] cdataElements)
throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
// configure an OutputFormat to handle CDATA
final OutputFormat of = new OutputFormat();
// specify which of your elements you want to be handled as CDATA.
// The use of the '^' between the namespaceURI and the localname
// seems to be an implementation detail of the xerces code.
// When processing xml that doesn't use namespaces, simply omit the
// namespace prefix as shown in the third CDataElement below.
of.setCDataElements(cdataElements);
// set any other options you'd like
of.setPreserveSpace(true);
of.setIndenting(true);
// create the serializer
final XMLSerializer serializer = new XMLSerializer(of);
serializer.setOutputByteStream(os);
return serializer;
}
private static void addResponseDoc(final MethodDoc methodDoc,
final MethodDocType methodDocType) {
final ResponseDocType responseDoc = new ResponseDocType();
final Tag returnTag = getSingleTagOrNull(methodDoc, "return");
if (returnTag != null) {
responseDoc.setReturnDoc(returnTag.text());
}
final Tag[] responseParamTags = methodDoc.tags("response.param");
for (final Tag responseParamTag : responseParamTags) {
// LOG.info( "Have responseparam tag: " + print( responseParamTag ) );
final WadlParamType wadlParam = new WadlParamType();
for (final Tag inlineTag : responseParamTag.inlineTags()) {
final String tagName = inlineTag.name();
final String tagText = inlineTag.text();
/* skip empty tags
*/
if (isEmpty(tagText)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Skipping empty inline tag of @response.param in method "
+ methodDoc.qualifiedName() + ": " + tagName);
}
continue;
}
switch (tagName) {
case "@name":
wadlParam.setName(tagText);
break;
case "@style":
wadlParam.setStyle(tagText);
break;
case "@type":
wadlParam.setType(QName.valueOf(tagText));
break;
case "@doc":
wadlParam.setDoc(tagText);
break;
default:
LOG.warning("Unknown inline tag of @response.param in method "
+ methodDoc.qualifiedName() + ": " + tagName
+ " (value: " + tagText + ")");
break;
}
}
responseDoc.getWadlParams().add(wadlParam);
}
final Map<String, List<Tag>> tagsByStatus = getResponseRepresentationTags(methodDoc);
for (final Entry<String, List<Tag>> entry : tagsByStatus.entrySet()) {
final RepresentationDocType representationDoc = new RepresentationDocType();
representationDoc.setStatus(Long.valueOf(entry.getKey()));
for (final Tag tag : entry.getValue()) {
if (tag.name().endsWith(".qname")) {
representationDoc.setElement(QName.valueOf(tag.text()));
} else if (tag.name().endsWith(".mediaType")) {
representationDoc.setMediaType(tag.text());
} else if (tag.name().endsWith(".example")) {
representationDoc.setExample(getSerializedExample(tag));
} else if (tag.name().endsWith(".doc")) {
representationDoc.setDoc(tag.text());
} else {
LOG.warning("Unknown response representation tag " + tag.name());
}
}
responseDoc.getRepresentations().add(representationDoc);
}
methodDocType.setResponseDoc(responseDoc);
}
private static boolean isEmpty(final String value) {
return value == null || value.isEmpty() || value.trim().isEmpty();
}
private static void addRequestRepresentationDoc(final MethodDoc methodDoc,
final MethodDocType methodDocType) {
final Tag requestElement = getSingleTagOrNull(methodDoc, "request.representation.qname");
final Tag requestExample = getSingleTagOrNull(methodDoc, "request.representation.example");
if (requestElement != null || requestExample != null) {
final RequestDocType requestDoc = new RequestDocType();
final RepresentationDocType representationDoc = new RepresentationDocType();
/* requestElement exists
*/
if (requestElement != null) {
representationDoc.setElement(QName.valueOf(requestElement.text()));
}
/* requestExample exists
*/
if (requestExample != null) {
final String example = getSerializedExample(requestExample);
if (!isEmpty(example)) {
representationDoc.setExample(example);
} else {
LOG.warning("Could not get serialized example for method " + methodDoc.qualifiedName());
}
}
requestDoc.setRepresentationDoc(representationDoc);
methodDocType.setRequestDoc(requestDoc);
}
}
private static Map<String, List<Tag>> getResponseRepresentationTags(final MethodDoc methodDoc) {
final Map<String, List<Tag>> tagsByStatus = new HashMap<>();
for (final Tag tag : methodDoc.tags()) {
final Matcher matcher = PATTERN_RESPONSE_REPRESENTATION.matcher(tag.name());
if (matcher.matches()) {
final String status = matcher.group(1);
List<Tag> tags = tagsByStatus.get(status);
if (tags == null) {
tags = new ArrayList<>();
tagsByStatus.put(status, tags);
}
tags.add(tag);
}
}
return tagsByStatus;
}
/**
* Searches an <code>@link</code> tag within the inline tags of the specified tag
* and serializes the referenced instance.
*
* @param tag the tag containing the inline tags to be searched.
* @return the {@code String} representation of the {@link com.sun.javadoc.Tag} or null if the parameter is null.
*/
private static String getSerializedExample(final Tag tag) {
if (tag != null) {
final Tag[] inlineTags = tag.inlineTags();
if (inlineTags != null && inlineTags.length > 0) {
for (final Tag inlineTag : inlineTags) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Have inline tag: " + print(inlineTag));
}
if ("@link".equals(inlineTag.name())) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Have link: " + print(inlineTag));
}
final SeeTag linkTag = (SeeTag) inlineTag;
return getSerializedLinkFromTag(linkTag);
} else if (!isEmpty(inlineTag.text())) {
return inlineTag.text();
}
}
} else {
LOG.fine("Have example: " + print(tag));
return tag.text();
}
}
return null;
}
private static Tag getSingleTagOrNull(final MethodDoc methodDoc, final String tagName) {
final Tag[] tags = methodDoc.tags(tagName);
if (tags != null && tags.length == 1) {
return tags[0];
}
return null;
}
private static void addParamDocs(final MethodDoc methodDoc,
final MethodDocType methodDocType,
final DocProcessor docProcessor) {
final Parameter[] parameters = methodDoc.parameters();
final ParamTag[] paramTags = methodDoc.paramTags();
/* only use both javadoc and reflection information when the number
* of params are the same
*/
if (parameters != null && paramTags != null
&& parameters.length == paramTags.length) {
for (int i = 0; i < parameters.length; i++) {
final Parameter parameter = parameters[i];
/* TODO: this only works if the params and tags are in the same
* order. If the param tags are mixed up, the comments for parameters
* will be wrong.
*/
final ParamTag paramTag = paramTags[i];
final ParamDocType paramDocType = new ParamDocType();
paramDocType.setParamName(paramTag.parameterName());
paramDocType.setCommentText(paramTag.parameterComment());
docProcessor.processParamTag(paramTag, parameter, paramDocType);
final AnnotationDesc[] annotations = parameter.annotations();
if (annotations != null) {
for (final AnnotationDesc annotationDesc : annotations) {
final AnnotationDocType annotationDocType = new AnnotationDocType();
final String typeName = annotationDesc.annotationType().qualifiedName();
annotationDocType.setAnnotationTypeName(typeName);
for (final ElementValuePair elementValuePair : annotationDesc.elementValues()) {
final NamedValueType namedValueType = new NamedValueType();
namedValueType.setName(elementValuePair.element().name());
namedValueType.setValue(elementValuePair.value().value().toString());
annotationDocType.getAttributeDocs().add(namedValueType);
}
paramDocType.getAnnotationDocs().add(annotationDocType);
}
}
methodDocType.getParamDocs().add(paramDocType);
}
}
}
private static String getSerializedLinkFromTag(final SeeTag linkTag) {
final MemberDoc referencedMember = linkTag.referencedMember();
if (referencedMember == null) {
throw new NullPointerException("Referenced member of @link " + print(linkTag) + " cannot be resolved.");
}
if (!referencedMember.isStatic()) {
LOG.warning("Referenced member of @link " + print(linkTag) + " is not static."
+ " Right now only references to static members are supported.");
return null;
}
/* Get referenced example bean
*/
final ClassDoc containingClass = referencedMember.containingClass();
final Object object;
try {
final Field declaredField = Class.forName(containingClass.qualifiedName(), false, Thread.currentThread()
.getContextClassLoader()).getDeclaredField(referencedMember.name());
if (referencedMember.isFinal()) {
declaredField.setAccessible(true);
}
object = declaredField.get(null);
LOG.log(Level.FINE, "Got object " + object);
} catch (final Exception e) {
LOG.info("Have classloader: " + ResourceDoclet.class.getClassLoader().getClass());
LOG.info("Have thread classloader " + Thread.currentThread().getContextClassLoader().getClass());
LOG.info("Have system classloader " + ClassLoader.getSystemClassLoader().getClass());
LOG.log(Level.SEVERE, "Could not get field " + referencedMember.qualifiedName(), e);
return null;
}
/* marshal the bean to xml
*/
try {
final JAXBContext jaxbContext = JAXBContext.newInstance(object.getClass());
final StringWriter stringWriter = new StringWriter();
final Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(object, stringWriter);
final String result = stringWriter.getBuffer().toString();
LOG.log(Level.FINE, "Got marshalled output:\n" + result);
return result;
} catch (final Exception e) {
LOG.log(Level.SEVERE, "Could serialize bean to xml: " + object, e);
return null;
}
}
private static String print(final Tag tag) {
return String.valueOf(tag.getClass()) + "["
+ "firstSentenceTags=" + toCSV(tag.firstSentenceTags())
+ ", inlineTags=" + toCSV(tag.inlineTags())
+ ", kind=" + tag.kind()
+ ", name=" + tag.name()
+ ", text=" + tag.text()
+ "]";
}
static String toCSV(final Tag[] items) {
if (items == null) {
return null;
}
return toCSV(Arrays.asList(items));
}
static String toCSV(final Collection<Tag> items) {
return toCSV(items, ", ", null);
}
static String toCSV(final Collection<Tag> items, final String separator, final String delimiter) {
if (items == null) {
return null;
}
if (items.isEmpty()) {
return "";
}
final StringBuilder sb = new StringBuilder();
for (final Iterator<Tag> iter = items.iterator(); iter.hasNext(); ) {
if (delimiter != null) {
sb.append(delimiter);
}
final Tag item = iter.next();
sb.append(item.name());
if (delimiter != null) {
sb.append(delimiter);
}
if (iter.hasNext()) {
sb.append(separator);
}
}
return sb.toString();
}
/**
* Return array length for given option: 1 + the number of arguments that
* the option takes.
*
* @param option option
* @return the number of args for the specified option
*/
public static int optionLength(final String option) {
LOG.fine("Invoked with option " + option);
if (OPTION_OUTPUT.equals(option)
|| OPTION_CLASSPATH.equals(option)
|| OPTION_DOC_PROCESSORS.equals(option)) {
return 2;
}
return 0;
}
/**
* Validate options.
*
* @param options options to be validated
* @param reporter {@link com.sun.javadoc.DocErrorReporter} for collecting eventual errors
* @return if the specified options are valid
*/
public static boolean validOptions(final String[][] options, final DocErrorReporter reporter) {
return validOption(OPTION_OUTPUT, "<path-to-file>", options, reporter)
&& validOption(OPTION_CLASSPATH, "<path>", options, reporter);
}
private static boolean validOption(final String optionName,
final String reportOptionName,
final String[][] options,
final DocErrorReporter reporter) {
final String option = getOptionArg(options, optionName);
final boolean foundOption = option != null && !option.trim().isEmpty();
if (!foundOption) {
reporter.printError(optionName + " " + reportOptionName + " must be specified.");
}
return foundOption;
}
private static String getOptionArg(final String[][] options, final String option) {
for (final String[] opt : options) {
if (opt[0].equals(option)) {
return opt[1];
}
}
return null;
}
static class Loader extends URLClassLoader {
public Loader(final String[] paths, final ClassLoader parent) {
super(getURLs(paths), parent);
}
Loader(final String[] paths) {
super(getURLs(paths));
}
private static URL[] getURLs(final String[] paths) {
final List<URL> urls = new ArrayList<>();
for (final String path : paths) {
try {
urls.add(new File(path).toURI().toURL());
} catch (final MalformedURLException e) {
throw new RuntimeException(e);
}
}
return urls.toArray(new URL[urls.size()]);
}
}
}